組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

5.5 マルチタスク環境でのC++

Cもそうですが,C++でも,標準規格の範囲ではマルチタスクに関してはいっさい触れられていません.また,マルチタスク環境を提供する組込み向けのOSは,単なるライブラリとしての形式で,処理系の外で提供されることも少なくありません.そのため,言語レベルで提供されている機能の中には,マルチタスク環境では特別な配慮を行う必要があるものが出てきます.

5.5.1 局所的な静的オブジェクトの初期化

「3.5.2 局所的な静的記憶域期間を持つオブジェクトの初期化」でも説明しましたが,局所的な静的オブジェクトは,実行パスが最初にそのオブジェクトの定義場所に差しかかったときに,初めて動的初期化が行われます.したがって,マルチタスク環境では,複数のタスクによって同時に同じオブジェクトの動的初期化が行われてしまう可能性があり,これを防ぐには何らかの排他制御が必要になります.

void func()
{
    lock(mutex);
    static Type object;
    unlock(mutex);
     …
}

上記のサンプルコードのように,局所的な静的オブジェクトの定義直前でロックをかけ,定義直後でロックを解除することで,排他制御を行うことは可能です.しかし,これでは,objectの動的初期化が完了した後でも,func関数を呼び出すたびにlock/unlock操作が行われてしまい,非常にオーバーヘッドが大きくなります.

この問題を回避するには次のようにします.

void func()
{
    static Type* ptr = 0;
    if (ptr == 0)
    {
        lock(mutex);
        if (ptr == 0)
        {
            static Type object;
            ptr = &object;
        }
        unlock(mutex);
    }
     …
}

上記のサンプルコードでは,objectへのアクセスはTypeへのポインタ型であるptrを使って行うことになります.ptrは空ポインタに初期化されている局所的な静的オブジェクトです.単なるポインタは静的初期化しか行われませんので,マルチタスク環境であっても排他制御は不要です.はじめに,ptrが空ポインタであるかどうかを判定し,空ポインタであればobjectが未初期化であることがわかります.この段階で初めてロックをかけます.最初にptrの値を評価した時点からlockを呼び出すまでの間にptrの値が変化した可能性があるため,ロックをかけた後,再度ptrの値を確認します.ここでも空ポインタであれば,初めてobjectの動的初期化を行い,objectへのポインタをptrに格納します.こうすることで,objectが初期化された後は,単にptrが空ポインタかどうかを1回判定するだけで済むようになり,オーバーヘッドを最小限に抑えることができるようになります.このテクニックは「Double-Chekced Locking」と呼ばれ,マルチタスク環境における重要なイディオムになっています.

5.5.2 例外処理

マルチタスク環境に対応するうえで,最も難度が高いのが例外処理です.例外処理が正しく動作するには,プログラムの開始位置から送出式までの文脈を,何らかの形で記憶しておく必要があります.しかし,例外処理はライブラリとして実装されている機能ではなく,言語レベルで提供される機能であるため,原則としてその詳細な仕組みは利用者に公開されていません.

マルチタスク環境で例外処理を使用するためには,LinuxやVxWorksのように,はじめからマルチタスク環境にコンパイラが対応しているか,ABI関数を再定義するなど,何らかの方法でコンパイラをカスタマイズできる必要があります.そうでなければ,マルチタスク環境で例外処理を使用することはできません.「4.4 組込み開発では例外処理を使用すべきか?」で触れた,システムから例外処理の機能を完全に一掃する方法を選択するしかなくなります.

5.5.3 new演算子とdelete演算子

new演算子とdelete演算子も,当然のことながら排他制御が必要になります.例外処理とは違って,new演算子とdelete演算子は再定義が可能ですので,マルチタスク環境ではデフォルトのnew演算子とdelete演算子は使用せず,位置指定形式以外のものをすべて定義し直すことで対応可能です.あるいは,内部的にmalloc関数やfree関数が呼び出されている場合,それらの関数の内部で排他制御できるのであれば,ほとんどの処理系ではそれだけで十分です.

なお,set_new_handler関数で登録したハンドラは静的な変数に格納されているはずですから,関数へのポインタ値の取得がアトミックオペレーションで行われない場合には,その部分に対する排他制御も必要になることでしょう.

5.5.4 volatile修飾子を活用する

引数にポインタや参照を受け取る関数や,クラスのメンバー関数は,const修飾子の有無によって多重定義することができます.それと同じように,volatile修飾子の有無によっても多重定義することができます.本来の意味とは若干異なるかもしれませんが,volatile修飾子の有無によって,排他制御など,マルチタスク環境に対応した処理を行うかどうかを切り替えることができます.たとえば,次のように,volatile修飾子がなければ,カウントアップする際に排他制御は行いませんが,volatile修飾子を付けた場合には,カウントアップの際に排他制御を行います.同じクラスであっても,同じタスクからしかアクセスされない場合にはvolatile修飾子なしで,複数のタスクからアクセスされる場合はvolatile修飾子を付けることで,簡単に排他制御の有無を切り替えることができます.しかも,この方法であれば,排他制御の有無を表すフラグをデータメンバーとして持つ場合とは異なり,まったくオーバーヘッドがありません.

class counter
{
public:
    explicit counter(int init = 0) : value(init) {}
    int get() const volatile { return value; }
    void up() { ++value; }
    void up() volatile
    {
        lock(mutex);
        ++value;
        unlock(mutex);
    }
     …
private:
    int value;
};
counter x;          // ← 排他制御なし版 
volatile counter y; // ← 排他制御あり版